#ifndef __NBTFILE__
#define __NBTFILE__

#include "NBTTagBase.h"
#include "..\..\IO\BinaryReader.h"

#include <wx\wfstream.h>
#include <wx\zstream.h>

/// <summary>
/// Represents an NBT file and provides the functionality.
/// </summary>
class NBTFile
{
#pragma region Internal stuff
	BinaryReader _reader;
	
	template<typename T, NBTType _type>
	NBTTagList<T, _type> *ReadValueList(const int &length)
	{
		auto *list = new NBTTagList<T, _type>();

		for (int i = 0; i < length; ++i)
			list->Value.push_back(_reader.ReadBigEndian<T>());

		return list;
	}

	template<>
	NBTTagList<std::string, TAG_String> *ReadValueList(const int &length)
	{
		auto *list = new NBTTagList<std::string, TAG_String>();

		for (int i = 0; i < length; ++i)
			list->Value.push_back(_reader.ReadString());

		return list;
	}

	NBTTagBase *ReadList()
	{
		auto type = (NBTType)_reader.ReadLittleEndian<byte>();
		auto length = _reader.ReadBigEndian<int>();

		switch (type)
		{
		case TAG_Byte:
			return ReadValueList<byte, TAG_Byte>(length);
		case TAG_Short:
			return ReadValueList<short, TAG_Short>(length);
		case TAG_Int:
			return ReadValueList<int, TAG_Int>(length);
		case TAG_Long:
			return ReadValueList<long long, TAG_Long>(length);
		case TAG_Float:
			return ReadValueList<float, TAG_Float>(length);
		case TAG_Double:
			return ReadValueList<double, TAG_Double>(length);
		case TAG_String:
			return ReadValueList<std::string, TAG_String>(length);
		// is ByteArray/IntArray supported in TAG_List?
		case TAG_Compound:
			{
				auto *list = new NBTTagList<NBTTagBase*, TAG_Compound>();

				for (int i = 0; i < length; ++i)
				{
					auto *compound = new NBTTagCompound();
					NBTTagBase *tag;
				
					while (tag = ReadTag(compound, true)) 
						compound->Value.push_back(tag);

					list->Value.push_back(compound);
				}

				return list;
			}
			break;
		case TAG_List:
			{
				auto list = new NBTTagList<NBTTagBase*, TAG_List>();

				for (int i = 0; i < length; ++i)
					list->Value.push_back(ReadList());
			};
			break;
		}
		

		return nullptr;
	}

	NBTTagBase *ReadTypedTag(NBTType &type, std::string &name)
	{
		switch (type)
		{
		case TAG_Byte:
			return new NBTTagValue<byte>(_reader.ReadLittleEndian<byte>());
		case TAG_Short:
			return new NBTTagValue<short>(_reader.ReadBigEndian<short>());
		case TAG_Int:
			return new NBTTagValue<int>(_reader.ReadBigEndian<int>());
		case TAG_Long:
			return new NBTTagValue<long long>(_reader.ReadBigEndian<long long>());
		case TAG_Float:
			return new NBTTagValue<float>(_reader.ReadBigEndian<float>());
		case TAG_Double:
			return new NBTTagValue<double>(_reader.ReadBigEndian<double>());
		case TAG_String:
			return new NBTTagValue<std::string>(_reader.ReadString());
		case TAG_ByteArray:
			{
				auto list = new NBTTagList<byte, TAG_Byte>();
				int length = _reader.ReadBigEndian<int>();

				list->Value.resize(length);
				_reader.ReadBytes((byte*)list->Value.data(), length * sizeof(byte));

				return list;
			}
			break;
		case TAG_IntArray:
			{
				auto list = new NBTTagList<int, TAG_Int>();
				int length = _reader.ReadBigEndian<int>();

				list->Value.resize(length);
				_reader.ReadBytes((byte*)list->Value.data(), length * sizeof(int));

				return list;
			}
			break;
		case TAG_List:
			return ReadList();
		case TAG_Compound:
			{
				auto *list = new NBTTagCompound();
				NBTTagBase *tag;
				
				while (tag = ReadTag(list, true)) 
					list->Value.push_back(tag);

				return list;
			}
			break;
		}

		return nullptr;
	}

	NBTTagBase *ReadTag(NBTTagBase *parent, bool named)
	{
		auto type = (NBTType)_reader.ReadLittleEndian<byte>();

		if (type == TAG_End)
			return nullptr;

		std::string name;

		if (named)
			name = _reader.ReadString();

		auto tag = ReadTypedTag(type, name);
		tag->Type = type;
		tag->Name = name;
		tag->Parent = parent;

		return tag;
	}

	void LoadFromStreamInternal(wxInputStream &file)
	{
		_reader.Stream = &file;
		Root = ReadTag(nullptr, true);
	}
#pragma endregion
public:

	/// <summary>
	/// The root compound tag that all other tags
	/// spawn from.
	/// </summary>
	NBTTagBase *Root;

	NBTFile() :
		Root(nullptr)
	{
	};

	~NBTFile()
	{
		if (Root != nullptr)
			delete Root;
	}

	/// <summary>
	/// Load either a compressed or uncompressed NBT structure from a file and fill the Root member with its contents
	/// </summary>
	/// <param name="fileName">The file to read from</param>
	/// <param name="compressed">True if the contents are compressed</param>
	void Load(const std::string &fileName, bool compressed)
	{
		wxFileInputStream file(fileName);

		if (compressed)
		{
			wxZlibInputStream gz(file);
			LoadFromStreamInternal(gz);
		}
		else
			LoadFromStreamInternal(file);
	}

	/// <summary>
	/// Load an uncompressed NBT structure from a file and fill the Root member with its contents.
	/// </summary>
	/// <param name="fileName">The file to read from</param>
	void Load(const std::string &fileName)
	{
		Load(fileName, false);
	}
};
#endif